// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.

use super::analysis;
use super::document_source::DocumentSource;
use super::text::LineIndex;
use super::tsc;
use super::urls::INVALID_SPECIFIER;

use crate::file_fetcher::get_source_from_bytes;
use crate::file_fetcher::map_content_type;
use crate::file_fetcher::SUPPORTED_SCHEMES;
use crate::http_cache;
use crate::http_cache::HttpCache;
use crate::text_encoding;

use deno_ast::MediaType;
use deno_core::error::anyhow;
use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_core::serde_json;
use deno_core::ModuleSpecifier;
use import_map::ImportMap;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::SystemTime;
use tsc::NavigationTree;

fn get_remote_headers(
  cache_filename: &Path,
) -> Option<HashMap<String, String>> {
  let metadata_path = http_cache::Metadata::filename(cache_filename);
  let metadata_str = fs::read_to_string(metadata_path).ok()?;
  let metadata: http_cache::Metadata =
    serde_json::from_str(&metadata_str).ok()?;
  Some(metadata.headers)
}

fn resolve_remote_specifier(
  specifier: &ModuleSpecifier,
  http_cache: &HttpCache,
  redirect_limit: isize,
) -> Option<ModuleSpecifier> {
  let cache_filename = http_cache.get_cache_filename(specifier)?;
  if redirect_limit >= 0 && cache_filename.is_file() {
    let headers = get_remote_headers(&cache_filename)?;
    if let Some(location) = headers.get("location") {
      let redirect =
        deno_core::resolve_import(location, specifier.as_str()).ok()?;
      resolve_remote_specifier(&redirect, http_cache, redirect_limit - 1)
    } else {
      Some(specifier.clone())
    }
  } else {
    None
  }
}

fn resolve_specifier(
  specifier: &ModuleSpecifier,
  redirects: &mut HashMap<ModuleSpecifier, ModuleSpecifier>,
  http_cache: &HttpCache,
) -> Option<ModuleSpecifier> {
  let scheme = specifier.scheme();
  if !SUPPORTED_SCHEMES.contains(&scheme) {
    return None;
  }

  if scheme == "data" {
    Some(specifier.clone())
  } else if scheme == "file" {
    let path = specifier.to_file_path().ok()?;
    if path.is_file() {
      Some(specifier.clone())
    } else {
      None
    }
  } else if let Some(specifier) = redirects.get(specifier) {
    Some(specifier.clone())
  } else {
    let redirect = resolve_remote_specifier(specifier, http_cache, 10)?;
    redirects.insert(specifier.clone(), redirect.clone());
    Some(redirect)
  }
}

#[derive(Debug, Clone)]
struct Metadata {
  dependencies: Option<HashMap<String, analysis::Dependency>>,
  length_utf16: usize,
  maybe_navigation_tree: Option<tsc::NavigationTree>,
  maybe_types: Option<analysis::ResolvedDependency>,
  maybe_warning: Option<String>,
  media_type: MediaType,
  source: DocumentSource,
  specifier: ModuleSpecifier,
  version: String,
}

impl Default for Metadata {
  fn default() -> Self {
    Self {
      dependencies: None,
      length_utf16: 0,
      maybe_navigation_tree: None,
      maybe_types: None,
      maybe_warning: None,
      media_type: MediaType::default(),
      source: DocumentSource::new(
        &INVALID_SPECIFIER,
        MediaType::default(),
        Arc::new(String::default()),
        LineIndex::default(),
      ),
      specifier: INVALID_SPECIFIER.clone(),
      version: String::default(),
    }
  }
}

impl Metadata {
  fn new(
    specifier: &ModuleSpecifier,
    source: Arc<String>,
    version: &str,
    media_type: MediaType,
    maybe_warning: Option<String>,
    maybe_import_map: &Option<ImportMap>,
  ) -> Self {
    let line_index = LineIndex::new(&source);
    let document_source =
      DocumentSource::new(specifier, media_type, source, line_index);
    let (dependencies, maybe_types) =
      if let Some(Ok(parsed_module)) = document_source.module() {
        let (deps, maybe_types) = analysis::analyze_dependencies(
          specifier,
          media_type,
          parsed_module,
          maybe_import_map,
        );
        (Some(deps), maybe_types)
      } else {
        (None, None)
      };

    Self {
      dependencies,
      length_utf16: document_source
        .text_info()
        .text_str()
        .encode_utf16()
        .count(),
      maybe_navigation_tree: None,
      maybe_types,
      maybe_warning,
      media_type: media_type.to_owned(),
      source: document_source,
      specifier: specifier.clone(),
      version: version.to_string(),
    }
  }

  fn refresh(&mut self, maybe_import_map: &Option<ImportMap>) {
    let (dependencies, maybe_types) =
      if let Some(Ok(parsed_module)) = self.source.module() {
        let (deps, maybe_types) = analysis::analyze_dependencies(
          &self.specifier,
          self.media_type,
          parsed_module,
          maybe_import_map,
        );
        (Some(deps), maybe_types)
      } else {
        (None, None)
      };
    self.dependencies = dependencies;
    self.maybe_types = maybe_types;
  }
}

#[derive(Debug, Clone, Default)]
struct Inner {
  http_cache: HttpCache,
  maybe_import_map: Option<ImportMap>,
  metadata: HashMap<ModuleSpecifier, Metadata>,
  redirects: HashMap<ModuleSpecifier, ModuleSpecifier>,
  remotes: HashMap<ModuleSpecifier, PathBuf>,
}

#[derive(Debug, Clone, Default)]
pub struct Sources(Arc<Mutex<Inner>>);

impl Sources {
  pub fn new(location: &Path) -> Self {
    Self(Arc::new(Mutex::new(Inner::new(location))))
  }

  pub fn contains_key(&self, specifier: &ModuleSpecifier) -> bool {
    self.0.lock().contains_key(specifier)
  }

  pub fn get_line_index(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Option<LineIndex> {
    self.0.lock().get_line_index(specifier)
  }

  pub fn get_maybe_types(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Option<analysis::ResolvedDependency> {
    self.0.lock().get_maybe_types(specifier)
  }

  pub fn get_maybe_warning(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Option<String> {
    self.0.lock().get_maybe_warning(specifier)
  }

  pub fn get_media_type(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Option<MediaType> {
    self.0.lock().get_media_type(specifier)
  }

  pub fn get_navigation_tree(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Option<tsc::NavigationTree> {
    self.0.lock().get_navigation_tree(specifier)
  }

  pub fn get_script_version(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Option<String> {
    self.0.lock().get_script_version(specifier)
  }

  pub fn get_source(&self, specifier: &ModuleSpecifier) -> Option<Arc<String>> {
    self.0.lock().get_source(specifier)
  }

  pub fn len(&self) -> usize {
    self.0.lock().metadata.len()
  }

  pub fn resolve_import(
    &self,
    specifier: &str,
    referrer: &ModuleSpecifier,
  ) -> Option<(ModuleSpecifier, MediaType)> {
    self.0.lock().resolve_import(specifier, referrer)
  }

  pub fn specifiers(&self) -> Vec<ModuleSpecifier> {
    self.0.lock().metadata.keys().cloned().collect()
  }

  pub fn set_import_map(&self, maybe_import_map: Option<ImportMap>) {
    self.0.lock().set_import_map(maybe_import_map)
  }

  pub fn set_navigation_tree(
    &self,
    specifier: &ModuleSpecifier,
    navigation_tree: tsc::NavigationTree,
  ) -> Result<(), AnyError> {
    self
      .0
      .lock()
      .set_navigation_tree(specifier, navigation_tree)
  }
}

impl Inner {
  fn new(location: &Path) -> Self {
    Self {
      http_cache: HttpCache::new(location),
      ..Default::default()
    }
  }

  fn calculate_script_version(
    &mut self,
    specifier: &ModuleSpecifier,
  ) -> Option<String> {
    let path = self.get_path(specifier)?;
    let metadata = fs::metadata(path).ok()?;
    if let Ok(modified) = metadata.modified() {
      if let Ok(n) = modified.duration_since(SystemTime::UNIX_EPOCH) {
        Some(format!("{}", n.as_millis()))
      } else {
        Some("1".to_string())
      }
    } else {
      Some("1".to_string())
    }
  }

  fn contains_key(&mut self, specifier: &ModuleSpecifier) -> bool {
    if let Some(specifier) =
      resolve_specifier(specifier, &mut self.redirects, &self.http_cache)
    {
      if self.get_metadata(&specifier).is_some() {
        return true;
      }
    }
    false
  }

  fn get_line_index(
    &mut self,
    specifier: &ModuleSpecifier,
  ) -> Option<LineIndex> {
    let specifier =
      resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?;
    let metadata = self.get_metadata(&specifier)?;
    Some(metadata.source.line_index().clone())
  }

  fn get_maybe_types(
    &mut self,
    specifier: &ModuleSpecifier,
  ) -> Option<analysis::ResolvedDependency> {
    let specifier =
      resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?;
    let metadata = self.get_metadata(&specifier)?;
    metadata.maybe_types
  }

  fn get_maybe_warning(
    &mut self,
    specifier: &ModuleSpecifier,
  ) -> Option<String> {
    let metadata = self.get_metadata(specifier)?;
    metadata.maybe_warning
  }

  fn get_media_type(
    &mut self,
    specifier: &ModuleSpecifier,
  ) -> Option<MediaType> {
    let specifier =
      resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?;
    let metadata = self.get_metadata(&specifier)?;
    Some(metadata.media_type)
  }

  fn get_metadata(&mut self, specifier: &ModuleSpecifier) -> Option<Metadata> {
    if let Some(metadata) = self.metadata.get(specifier).cloned() {
      if metadata.version == self.calculate_script_version(specifier)? {
        return Some(metadata);
      }
    }

    let version = self.calculate_script_version(specifier)?;
    let path = self.get_path(specifier)?;
    let bytes = fs::read(path).ok()?;
    let scheme = specifier.scheme();
    let (source, media_type, maybe_types, maybe_warning) = if scheme == "file" {
      let maybe_charset =
        Some(text_encoding::detect_charset(&bytes).to_string());
      let source = get_source_from_bytes(bytes, maybe_charset).ok()?;
      (source, MediaType::from(specifier), None, None)
    } else {
      let cache_filename = self.http_cache.get_cache_filename(specifier)?;
      let headers = get_remote_headers(&cache_filename)?;
      let maybe_content_type = headers.get("content-type").cloned();
      let (media_type, maybe_charset) =
        map_content_type(specifier, maybe_content_type);
      let source = get_source_from_bytes(bytes, maybe_charset).ok()?;
      let maybe_types = headers.get("x-typescript-types").map(|s| {
        analysis::resolve_import(s, specifier, &self.maybe_import_map)
      });
      let maybe_warning = headers.get("x-deno-warning").cloned();
      (source, media_type, maybe_types, maybe_warning)
    };
    let mut metadata = Metadata::new(
      specifier,
      Arc::new(source),
      &version,
      media_type,
      maybe_warning,
      &self.maybe_import_map,
    );
    if maybe_types.is_some() {
      metadata.maybe_types = maybe_types;
    }
    self.metadata.insert(specifier.clone(), metadata.clone());
    Some(metadata)
  }

  fn get_navigation_tree(
    &mut self,
    specifier: &ModuleSpecifier,
  ) -> Option<tsc::NavigationTree> {
    let specifier =
      resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?;
    let metadata = self.get_metadata(&specifier)?;
    metadata.maybe_navigation_tree
  }

  fn get_path(&mut self, specifier: &ModuleSpecifier) -> Option<PathBuf> {
    if specifier.scheme() == "file" {
      specifier.to_file_path().ok()
    } else if let Some(path) = self.remotes.get(specifier) {
      Some(path.clone())
    } else {
      let path = self.http_cache.get_cache_filename(specifier)?;
      if path.is_file() {
        self.remotes.insert(specifier.clone(), path.clone());
        Some(path)
      } else {
        None
      }
    }
  }

  fn get_script_version(
    &mut self,
    specifier: &ModuleSpecifier,
  ) -> Option<String> {
    let specifier =
      resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?;
    let metadata = self.get_metadata(&specifier)?;
    Some(metadata.version)
  }

  fn get_source(&mut self, specifier: &ModuleSpecifier) -> Option<Arc<String>> {
    let specifier =
      resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?;
    let metadata = self.get_metadata(&specifier)?;
    Some(metadata.source.text_info().text())
  }

  fn resolution_result(
    &mut self,
    specifier: &ModuleSpecifier,
  ) -> Option<(ModuleSpecifier, MediaType)> {
    let specifier =
      resolve_specifier(specifier, &mut self.redirects, &self.http_cache)?;
    let media_type = if let Some(metadata) = self.metadata.get(&specifier) {
      metadata.media_type
    } else {
      MediaType::from(&specifier)
    };
    Some((specifier, media_type))
  }

  fn resolve_import(
    &mut self,
    specifier: &str,
    referrer: &ModuleSpecifier,
  ) -> Option<(ModuleSpecifier, MediaType)> {
    let referrer =
      resolve_specifier(referrer, &mut self.redirects, &self.http_cache)?;
    let metadata = self.get_metadata(&referrer)?;
    let dependencies = &metadata.dependencies?;
    let dependency = dependencies.get(specifier)?;
    if let Some(type_dependency) = &dependency.maybe_type {
      if let analysis::ResolvedDependency::Resolved(resolved_specifier) =
        type_dependency
      {
        // even if we have a module in the maybe_types slot, it doesn't mean
        // that it is the actual module we should be using based on headers,
        // so we check here and update properly.
        if let Some(type_dependency) = self.get_maybe_types(resolved_specifier)
        {
          self.set_maybe_type(specifier, &referrer, &type_dependency);
          if let analysis::ResolvedDependency::Resolved(type_specifier) =
            type_dependency
          {
            self.resolution_result(&type_specifier)
          } else {
            self.resolution_result(resolved_specifier)
          }
        } else {
          self.resolution_result(resolved_specifier)
        }
      } else {
        None
      }
    } else {
      let code_dependency = &dependency.maybe_code.clone()?;
      if let analysis::ResolvedDependency::Resolved(resolved_specifier) =
        code_dependency
      {
        if let Some(type_dependency) = self.get_maybe_types(resolved_specifier)
        {
          self.set_maybe_type(specifier, &referrer, &type_dependency);
          if let analysis::ResolvedDependency::Resolved(type_specifier) =
            type_dependency
          {
            self.resolution_result(&type_specifier)
          } else {
            self.resolution_result(resolved_specifier)
          }
        } else {
          self.resolution_result(resolved_specifier)
        }
      } else {
        None
      }
    }
  }

  fn set_import_map(&mut self, maybe_import_map: Option<ImportMap>) {
    for (_, metadata) in self.metadata.iter_mut() {
      metadata.refresh(&maybe_import_map);
    }
    self.maybe_import_map = maybe_import_map;
  }

  fn set_maybe_type(
    &mut self,
    specifier: &str,
    referrer: &ModuleSpecifier,
    dependency: &analysis::ResolvedDependency,
  ) {
    if let Some(metadata) = self.metadata.get_mut(referrer) {
      if let Some(dependencies) = &mut metadata.dependencies {
        if let Some(dep) = dependencies.get_mut(specifier) {
          dep.maybe_type = Some(dependency.clone());
        }
      }
    }
  }

  fn set_navigation_tree(
    &mut self,
    specifier: &ModuleSpecifier,
    navigation_tree: NavigationTree,
  ) -> Result<(), AnyError> {
    let mut metadata = self
      .metadata
      .get_mut(specifier)
      .ok_or_else(|| anyhow!("Specifier not found {}"))?;
    metadata.maybe_navigation_tree = Some(navigation_tree);
    Ok(())
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use deno_core::resolve_path;
  use deno_core::resolve_url;
  use deno_core::serde_json::json;
  use tempfile::TempDir;

  fn setup() -> (Sources, PathBuf) {
    let temp_dir = TempDir::new().expect("could not create temp dir");
    let location = temp_dir.path().join("deps");
    let sources = Sources::new(&location);
    (sources, location)
  }

  #[test]
  fn test_sources_get_script_version() {
    let (sources, _) = setup();
    let tests = test_util::testdata_path();
    let specifier =
      resolve_path(&tests.join("001_hello.js").to_string_lossy()).unwrap();
    let actual = sources.get_script_version(&specifier);
    assert!(actual.is_some());
  }

  #[test]
  fn test_sources_get_text() {
    let (sources, _) = setup();
    let tests = test_util::testdata_path();
    let specifier =
      resolve_path(&tests.join("001_hello.js").to_string_lossy()).unwrap();
    let actual = sources.get_source(&specifier);
    assert!(actual.is_some());
    let actual = actual.unwrap().to_string();
    assert_eq!(actual, "console.log(\"Hello World\");\n");
  }

  #[test]
  fn test_resolve_dependency_types() {
    let (sources, location) = setup();
    let cache = HttpCache::new(&location);
    let specifier_dep = resolve_url("https://deno.land/x/mod.ts").unwrap();
    cache
      .set(
        &specifier_dep,
        Default::default(),
        b"export * from \"https://deno.land/x/lib.js\";",
      )
      .unwrap();
    let specifier_code = resolve_url("https://deno.land/x/lib.js").unwrap();
    let mut headers_code = HashMap::new();
    headers_code
      .insert("x-typescript-types".to_string(), "./lib.d.ts".to_string());
    cache
      .set(&specifier_code, headers_code, b"export const a = 1;")
      .unwrap();
    let specifier_type = resolve_url("https://deno.land/x/lib.d.ts").unwrap();
    cache
      .set(
        &specifier_type,
        Default::default(),
        b"export const a: number;",
      )
      .unwrap();
    let actual =
      sources.resolve_import("https://deno.land/x/lib.js", &specifier_dep);
    assert_eq!(actual, Some((specifier_type, MediaType::Dts)))
  }

  #[test]
  /// This is a regression test for https://github.com/denoland/deno/issues/10031
  fn test_resolve_dependency_import_types() {
    let (sources, location) = setup();
    let cache = HttpCache::new(&location);
    let specifier_dep = resolve_url("https://deno.land/x/mod.ts").unwrap();
    cache
      .set(
        &specifier_dep,
        Default::default(),
        b"import type { A } from \"https://deno.land/x/lib.js\";\nconst a: A = { a: \"a\" };",
      )
      .unwrap();
    let specifier_code = resolve_url("https://deno.land/x/lib.js").unwrap();
    let mut headers_code = HashMap::new();
    headers_code
      .insert("x-typescript-types".to_string(), "./lib.d.ts".to_string());
    cache
      .set(&specifier_code, headers_code, b"export const a = 1;")
      .unwrap();
    let specifier_type = resolve_url("https://deno.land/x/lib.d.ts").unwrap();
    cache
      .set(
        &specifier_type,
        Default::default(),
        b"export const a: number;\nexport interface A { a: number; }\n",
      )
      .unwrap();
    let actual =
      sources.resolve_import("https://deno.land/x/lib.js", &specifier_dep);
    assert_eq!(actual, Some((specifier_type, MediaType::Dts)))
  }

  #[test]
  fn test_warning_header() {
    let (sources, location) = setup();
    let cache = HttpCache::new(&location);
    let specifier = resolve_url("https://deno.land/x/lib.js").unwrap();
    let mut headers = HashMap::new();
    headers.insert(
      "x-deno-warning".to_string(),
      "this is a warning".to_string(),
    );
    cache
      .set(&specifier, headers, b"export const a = 1;")
      .unwrap();
    let actual = sources.get_maybe_warning(&specifier);
    assert_eq!(actual, Some("this is a warning".to_string()));
  }

  #[test]
  fn test_resolve_dependency_evil_redirect() {
    let (sources, location) = setup();
    let cache = HttpCache::new(&location);
    let evil_specifier = resolve_url("https://deno.land/x/evil.ts").unwrap();
    let mut evil_headers = HashMap::new();
    evil_headers
      .insert("location".to_string(), "file:///etc/passwd".to_string());
    cache.set(&evil_specifier, evil_headers, b"").unwrap();
    let remote_specifier = resolve_url("https://deno.land/x/mod.ts").unwrap();
    cache
      .set(
        &remote_specifier,
        Default::default(),
        b"export * from \"./evil.ts\";",
      )
      .unwrap();
    let actual = sources.resolve_import("./evil.ts", &remote_specifier);
    assert_eq!(actual, None);
  }

  #[test]
  fn test_resolve_with_import_map() {
    let (sources, location) = setup();
    let import_map_json = json!({
      "imports": {
        "mylib": "https://deno.land/x/myLib/index.js"
      }
    });
    let import_map = ImportMap::from_json(
      "https://deno.land/x/",
      &import_map_json.to_string(),
    )
    .unwrap();
    sources.set_import_map(Some(import_map));
    let cache = HttpCache::new(&location);
    let mylib_specifier =
      resolve_url("https://deno.land/x/myLib/index.js").unwrap();
    let mut mylib_headers_map = HashMap::new();
    mylib_headers_map.insert(
      "content-type".to_string(),
      "application/javascript".to_string(),
    );
    cache
      .set(
        &mylib_specifier,
        mylib_headers_map,
        b"export const a = \"a\";\n",
      )
      .unwrap();
    let referrer = resolve_url("https://deno.land/x/mod.ts").unwrap();
    cache
      .set(
        &referrer,
        Default::default(),
        b"export { a } from \"mylib\";",
      )
      .unwrap();
    let actual = sources.resolve_import("mylib", &referrer);
    assert_eq!(actual, Some((mylib_specifier, MediaType::JavaScript)));
  }

  #[test]
  fn test_update_import_map() {
    let (sources, location) = setup();
    let import_map_json = json!({
      "imports": {
        "otherlib": "https://deno.land/x/otherlib/index.js"
      }
    });
    let import_map = ImportMap::from_json(
      "https://deno.land/x/",
      &import_map_json.to_string(),
    )
    .unwrap();
    sources.set_import_map(Some(import_map));
    let cache = HttpCache::new(&location);
    let mylib_specifier =
      resolve_url("https://deno.land/x/myLib/index.js").unwrap();
    let mut mylib_headers_map = HashMap::new();
    mylib_headers_map.insert(
      "content-type".to_string(),
      "application/javascript".to_string(),
    );
    cache
      .set(
        &mylib_specifier,
        mylib_headers_map,
        b"export const a = \"a\";\n",
      )
      .unwrap();
    let referrer = resolve_url("https://deno.land/x/mod.ts").unwrap();
    cache
      .set(
        &referrer,
        Default::default(),
        b"export { a } from \"mylib\";",
      )
      .unwrap();
    let actual = sources.resolve_import("mylib", &referrer);
    assert_eq!(actual, None);
    let import_map_json = json!({
      "imports": {
        "otherlib": "https://deno.land/x/otherlib/index.js",
        "mylib": "https://deno.land/x/myLib/index.js"
      }
    });
    let import_map = ImportMap::from_json(
      "https://deno.land/x/",
      &import_map_json.to_string(),
    )
    .unwrap();
    sources.set_import_map(Some(import_map));
    let actual = sources.resolve_import("mylib", &referrer);
    assert_eq!(actual, Some((mylib_specifier, MediaType::JavaScript)));
  }

  #[test]
  fn test_sources_resolve_specifier_non_supported_schema() {
    let (sources, _) = setup();
    let specifier =
      resolve_url("foo://a/b/c.ts").expect("could not create specifier");
    let sources = sources.0.lock();
    let mut redirects = sources.redirects.clone();
    let http_cache = sources.http_cache.clone();
    let actual = resolve_specifier(&specifier, &mut redirects, &http_cache);
    assert!(actual.is_none());
  }
}
